#region File Description
//-----------------------------------------------------------------------------
// GameplayScreen.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion

#region Using Statements
using System;
using System.Collections.Generic;
using Arcadium.GameEntities;
using Arcadium.Graphics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.GamerServices;
using Arcadium.GameEntities.Projectiles;
using Arcadium.GameEntities.Characters;
#endregion

namespace Arcadium
{
    /// <summary>
    /// The game, all modes etc.
    /// </summary>
    class GameplayScreen : GameScreen
    {
        #region Fields

        ContentManager content;
        SpriteFont gameFont;

        float pauseAlpha;

        NetworkSession networkSession;

        PacketWriter packetWriter = new PacketWriter();
        PacketReader packetReader = new PacketReader();

        // How often should we send network packets?
        int framesBetweenPackets = 6;

        // How recently did we send the last network packet?
        int framesSinceLastSend;

        World world;

        #endregion

        #region Properties

        /// <summary>
        /// The logic for deciding whether the game is paused depends on whether
        /// this is a networked or single player game. If we are in a network session,
        /// we should go on updating the game even when the user tabs away from us or
        /// brings up the pause menu, because even though the local player is not
        /// responding to input, other remote players may not be paused. In single
        /// player modes, however, we want everything to pause if the game loses focus.
        /// </summary>
        new bool IsActive
        {
            get { return !IsExiting; }
        }

        #endregion

        #region Initialization

        /// <summary>
        /// Constructor.
        /// </summary>
        public GameplayScreen(NetworkSession networkSession)
        {
            this.networkSession = networkSession;

            TransitionOnTime = TimeSpan.FromSeconds(0.5);
            TransitionOffTime = TimeSpan.FromSeconds(0.5);

            if (networkSession.IsHost)
                this.world = new HostWorld(); // Has logic for enemy-spawn
            else
                this.world = new RemoteWorld();
        }


        /// <summary>
        /// Load graphics content for the game.
        /// </summary>
        public override void LoadContent()
        {
            if (content == null)
                content = new ContentManager(ScreenManager.Game.Services, "Content");

            // Load UI
            gameFont = content.Load<SpriteFont>("gamefont");

            // Load world
            world.LoadContent(content, new GameSessionProperties(networkSession.SessionProperties));

            // Load players
            foreach (NetworkGamer gamer in networkSession.AllGamers)
            {
                Player player = null;
                Character character = null;

                character = new NewCharacter(world);

                if (gamer.IsLocal)
                    player = new Player((gamer as LocalNetworkGamer).SignedInGamer.PlayerIndex, character);
                else
                    player = new Player(character);

                //// Create a character and a player object for this gamer.
                //if (gamer.IsLocal)
                //{
                //    LocalNetworkGamer localGamer = gamer as LocalNetworkGamer;
                //    character = new PlayableCharacter(world, gamer);
                //    player = new Player(localGamer.SignedInGamer.PlayerIndex, character);
                //}
                //else
                //{
                //    character = new RemoteCharacter(world, 1, CharacterType.Player);
                //    player = new Player(character);
                //}

                gamer.Tag = player;
            }

            // once the load has finished, we use ResetElapsedTime to tell the game's
            // timing mechanism that we have just finished a very long frame, and that
            // it should not try to catch up.
            ScreenManager.Game.ResetElapsedTime();
        }


        /// <summary>
        /// Unload graphics content used by the game.
        /// </summary>
        public override void UnloadContent()
        {
            content.Unload();
        }

        #endregion

        #region Update and Draw

        /// <summary>
        /// Updates the state of the game.
        /// </summary>
        public override void Update(GameTime gameTime, bool otherScreenHasFocus,
                                                       bool coveredByOtherScreen)
        {
            base.Update(gameTime, otherScreenHasFocus, false);

            // Gradually fade in or out depending on whether we are covered by the pause screen.
            if (coveredByOtherScreen)
                pauseAlpha = Math.Min(pauseAlpha + 1f / 32, 1);
            else
                pauseAlpha = Math.Max(pauseAlpha - 1f / 32, 0);

            if (IsActive)
            {
                // Is it time to send outgoing network packets?
                bool sendPacketThisFrame = false;

                framesSinceLastSend++;

                if (framesSinceLastSend >= framesBetweenPackets)
                {
                    sendPacketThisFrame = true;
                    framesSinceLastSend = 0;
                }

                foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
                {
                    // Update & send latest info about the player.
                    UpdateLocalGamer(gameTime, gamer, sendPacketThisFrame);
                }

                // Update & send latest info about the world.
                //UpdateMachine(gameTime, networkSession.LocalGamers[0]);

                // Read any packets telling us the positions of remotely controlled players.
                ReadIncomingPackets(gameTime, networkSession.LocalGamers[0]);

                //// Apply prediction and smoothing to the remotely controlled players.
                //foreach (NetworkGamer gamer in networkSession.RemoteGamers)
                //{
                //    Player player = gamer.Tag as Player;

                //    player.Update(gameTime);
                //}

                if (networkSession.SessionState == NetworkSessionState.Lobby)
                {
                    // TODO: Fix so people come back to correct menu etc. (MenuSidebar with current networksession, etc.)
                    LoadingScreen.Load(ScreenManager, true, null,
                                       new LobbyScreen(networkSession));
                }
            }
        }

        /// <summary>
        /// Helper for updating a locally controlled gamer.
        /// </summary>
        void UpdateLocalGamer(GameTime gameTime, LocalNetworkGamer gamer, bool sendPacketThisFrame)
        {
            // Updates done by each gamer for all machines
            Player player = gamer.Tag as Player;
            player.Update(gameTime);

            // Periodically send our state to everyone in the session.
            if (sendPacketThisFrame)
            {
                (player.Character as NewCharacter).WriteNetworkPacket(packetWriter, gameTime);

                gamer.SendData(packetWriter, SendDataOptions.InOrder);
            }
        }

        /// <summary>
        /// Helper for updating this machine (ergo, this world).
        /// </summary>
        void UpdateMachine(GameTime gameTime, LocalNetworkGamer gamer)
        {
            // Update the world.
            world.Update(gameTime);

            // Machine details.
            packetWriter.Write(true);

            #region Projectile spawns & deaths

            int projectilesAdded = world.Changes.ProjectilesAdded.Count;
            packetWriter.Write(projectilesAdded);
            foreach (Projectile projectile in world.Changes.ProjectilesAdded)
            {
                packetWriter.Write(projectile.Position);
            }

            int projectilesRemoved = world.Changes.ProjectilesRemoved.Count;
            packetWriter.Write(projectilesRemoved);
            foreach (Projectile projectile in world.Changes.ProjectilesRemoved)
            {
                packetWriter.Write(projectile.Id);
            }

            #endregion


            // Additionally, if this is the host-mashine, 
            if (networkSession.IsHost)
            {
                #region Enemy spawns & deaths

                bool enemyAdded = world.Changes.EnemyAdded != null;
                packetWriter.Write(enemyAdded);
                if (enemyAdded)
                {
                    packetWriter.Write(world.Changes.EnemyAdded.Id);
                    // TODO: HERE: Write the enemy so we can recreate a RemoteCharacter on the other machines.
                }

                bool enemyRemoved = world.Changes.EnemyRemoved != null;
                packetWriter.Write(enemyRemoved);
                if (enemyRemoved)
                {
                    packetWriter.Write(world.Changes.EnemyRemoved.Id);
                }

                #endregion

                packetWriter.Write(world.Enemies.Count);
                foreach (Character enemy in world.Enemies)
                {
                    packetWriter.Write(enemy.Id);
                    packetWriter.Write(enemy.Position);
                    packetWriter.Write(enemy.Direction == 1);
                }

                packetWriter.Write(world.Projectiles.Count);
                foreach (Projectile projectile in world.Projectiles)
                {
                    packetWriter.Write(projectile.Id);
                    packetWriter.Write(projectile.Position);
                    packetWriter.Write(projectile.Direction == 1);
                }

            }

            gamer.SendData(packetWriter, SendDataOptions.InOrder);
            world.Changes.Reset();
        }

        /// <summary>
        /// Helper for reading incoming network packets.
        /// </summary>
        void ReadIncomingPackets(GameTime gameTime, LocalNetworkGamer gamer)
        {
            // Keep reading as long as incoming packets are available.
            while (gamer.IsDataAvailable)
            {
                NetworkGamer sender;

                // Read a single packet from the network.
                gamer.ReceiveData(packetReader, out sender);

                // Discard packets sent by local gamers: we already know their state!
                if (sender.IsLocal)
                    continue;

                // Look up the player object associated with whoever sent this packet.
                Player remotePlayer = sender.Tag as Player;

                // Estimate how long this packet took to arrive.
                TimeSpan latency = TimeSpan.FromTicks(sender.RoundtripTime.Ticks / 2);

                (remotePlayer.Character as NewCharacter).ReadNetworkPacket(packetReader, gameTime, latency);

                //bool sentByMachine = packetReader.ReadBoolean();
                //if (!sentByMachine)
                //{
                //    remotePlayer.Character.Position = packetReader.ReadVector2();
                //}
                //else // The package we recieved is from the host, with the latest update.
                //{
                //    int projectilesAdded = packetReader.ReadInt32();
                //    for (int i = 0; i < projectilesAdded; i++)
                //    {
                //        world.AddEntity(StandardBullet.Create(world, sender, packetReader.ReadVector2(), Vector2.Zero));
                //    }

                //    int projectilesRemoved = packetReader.ReadInt32();
                //    for (int i = 0; i < projectilesRemoved; i++)
                //    {
                //        long id = packetReader.ReadInt64();
                //        for (int j = 0; j < world.Enemies.Count; j++)
                //            if (world.Projectiles[j].Id == id)
                //            {
                //                world.RemoveEntity(world.Projectiles[j]);
                //                break;
                //            }
                //    }

                //    if (sender.IsHost)
                //    {
                //        bool enemyAdded = packetReader.ReadBoolean();
                //        if (enemyAdded)
                //        {
                //            long id = packetReader.ReadInt64();
                //            //bool alreadyExits = false;
                //            //for (int i = 0; i < world.Enemies.Count; i++)
                //            //    if (world.Enemies[i].Id == id)
                //            //    {
                //            //        alreadyExits = true;
                //            //        break;
                //            //    }
                //            //if (!alreadyExits)
                //                world.AddEntity(SmallEnemyCharacter.Create(world, 1));
                //        }

                //        bool enemyRemoved = packetReader.ReadBoolean();
                //        if (enemyRemoved)
                //        {
                //            long id = packetReader.ReadInt64();
                //            for (int i = 0; i < world.Enemies.Count; i++)
                //                if (world.Enemies[i].Id == id)
                //                {
                //                    world.RemoveEntity(world.Enemies[i]);
                //                    break;
                //                }
                //        }

                //        // Update all positions
                //        int enemyCount = packetReader.ReadInt32();
                //        for (int i = 0; i < enemyCount; i++)
                //        {
                //            long id = packetReader.ReadInt64();
                //            world.EnemiesD[id].Position = packetReader.ReadVector2();
                //            world.EnemiesD[id].Direction = packetReader.ReadBoolean() ? 1 : -1;
                //        }

                //        // Update all positions
                //        int projectileCount = packetReader.ReadInt32();
                //        for (int i = 0; i < projectileCount; i++)
                //        {
                //            long id = packetReader.ReadInt64();
                //            world.ProjectilesD[id].Position = packetReader.ReadVector2();
                //            world.ProjectilesD[id].Direction = packetReader.ReadBoolean() ? 1 : -1;
                //        }
                //    }
                //}
            }
        }

        // Handels player-input.

        /// <summary>
        /// Lets the game respond to player input. Unlike the Update method,
        /// this will only be called when the gameplay screen is active.
        /// </summary>
        public override void HandleInput(InputState input)
        {
            if (input == null)
                throw new ArgumentNullException("input");

            // In network game modes, handle input for all the
            // local players who are participating in the session.
            foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
            {
                if (!HandlePlayerInput(input, gamer))
                    break;
            }
        }

        /// <summary>
        /// Handles input for the specified player. In local game modes, this is called
        /// just once for the controlling player. In network modes, it can be called
        /// more than once if there are multiple profiles playing on the local machine.
        /// Returns true if we should continue to handle input for subsequent players,
        /// or false if this player has paused the game.
        /// </summary>
        bool HandlePlayerInput(InputState input, LocalNetworkGamer gamer)
        {
            PlayerIndex playerIndex = gamer.SignedInGamer.PlayerIndex;
            // Look up inputs for the specified gamer
            KeyboardState keyboardState = input.CurrentKeyboardStates[(int)playerIndex];
            GamePadState gamePadState = input.CurrentGamePadStates[(int)playerIndex];

            // The game pauses either if the user presses the pause button, or if
            // they unplug the active gamepad. This requires us to keep track of
            // whether a gamepad was ever plugged in, because we don't want to pause
            // on PC if they are playing with a keyboard and have no gamepad at all!
            bool gamePadDisconnected = !gamePadState.IsConnected &&
                                       input.GamePadWasConnected[(int)playerIndex];

            if (input.IsPauseGame(playerIndex) || gamePadDisconnected)
            {
                ScreenManager.AddScreen(new PauseMenuScreen(networkSession), playerIndex);
                return false;
            }

            Player player = gamer.Tag as Player;
            player.HandleInput(input);

            return true;
        }

        // Drawing

        /// <summary>
        /// Draws the gameplay screen.
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            View.GetInstance().GraphicsDevice.Clear(ClearOptions.Target,
                                               Color.Black, 0, 0);

            SpriteBatch spriteBatch = ViewResource.SpriteBatch;

            spriteBatch.Begin();

            world.Draw(spriteBatch);

            foreach (NetworkGamer gamer in networkSession.AllGamers)
            {
                (gamer.Tag as Player).Character.Draw(spriteBatch);
            }

            string message = string.Format("NETWORK\nSessionState: {0}\nSessionType: {1}\nHost: {2}\n#Players: {3}\nBytesPerSecondSent: {5} bytes\nBytesPerSecondReceived: {6} bytes\n\n#Characters: {7}",
                networkSession.SessionState, networkSession.SessionType, networkSession.Host, 
                networkSession.AllGamers.Count, null, 
                networkSession.BytesPerSecondSent, networkSession.BytesPerSecondReceived,
                world.Enemies.Count);
            Vector2 messagePosition = new Vector2(100);
            spriteBatch.DrawString(gameFont, message, messagePosition, 
                Color.Black, 0, Vector2.Zero, .75f * View.ScreenScale, SpriteEffects.None, 0);

            spriteBatch.End();

            // If the game is transitioning on or off, fade it out to black.
            if (TransitionPosition > 0 || pauseAlpha > 0)
            {
                float alpha = MathHelper.Lerp(1f - TransitionAlpha, 1f, pauseAlpha / 2);

                View.GetInstance().FadeBackBufferToBlack(alpha);
            }
        }

        #endregion
    }
}